伪随机算法系列-05 Noise Variants(Fractals and Tiling)

翻译—-Pseudorandom Noise-Noise Variants

如果你觉得这篇教程不错,请去支持原作者

此教程使用的Unity版本为2020.3.12f1

  • 组合多个音阶(Octaves)来创建一个分析噪声
  • 引入扰动型(turbulence)柏林和值噪声
  • 增加一个选项来创建瓦片效果

这是伪随机噪声系列教程的第五篇.增加分形噪声,扰动型噪声和瓦片型布局.

A torus showing six octaves of lacunarity 3 fractal 3D Perlin noise.


1 分形噪声(Fractal Noise)

到目前为止,我们只使用了一个单一的柏林噪声或值噪声.虽然结果看起来是随机的,但是有很多大小相同的特征块.就算有变化,那也是基于同一个晶格.所有的变化都仅限于单一的缩放等级上,并且由域变换决定的.噪声在大幅度和小幅度上变化太少,轻易暴露了它是人为的本质.

我们可以在不同的缩放等级上再次对噪声进行采样,在第二个频率上引入变化.最简单的方法是,在缩放为1倍和2倍上分别采样.把两个采样值相加得到新的噪声,便得到了有大有小的变化.我们可以多来几次,把下一个较大倍率的采样加上较小的特征.最终的结果会因为小特征(微观)与大特征(宏观)相似而出现自相似性,所以被称为分形噪声.

1.1 噪声的设置(Noise Settings)

想要支持分形噪声,我们需要增加一些配置选项来控制它.为了便于将配置传递给Noise,先创建一个public结构体Noise.Settings,当前只包含一个整数字段seed.由于这个结构纯粹是为了方便使用配置选项,就直接把这个字段公开,并用System.Serializable属性把这个结构体标记为可序列化.这样,Unity就可以保存配置信息,方便我们使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
using Unity.Burst;


public static partial class Noise
{
[Serializable]
public struct Settings
{
public int seed;
}


}

结构体里的字段必须在使用前全部初始化.目前我们先添加一个静态方法Default来方便使用,初始化数值后面在弄.

1
2
3
4
5
6
public struct Settings
{
public int seed;

public static Settings Default => new Settings {};
}

修改Noise.Job代码用Settings作为参数代替seed和hash.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//public SmallXXHash4 hash;
public Settings settings;

public float3x4 domainTRS;

public void Execute(int i)
{
var hash = SmallXXHash4.Seed(settings.seed);
noise[i] = default(N).GetNoise4(domainTRS.TransformVectors(transpose(positions[i])), hash);
}

public static JobHandle ScheduleParallel(
NativeArray<float3x4> positions, NativeArray<float4> noise, //int seed,
Settings settings, SpaceTRS domainTRS, int resolution, JobHandle dependency)
=> new Job<N>
{
positions = positions,
noise = noise,
//hash = SmallXXHash.Seed(seed),
settings = settings,
domainTRS = domainTRS.Matrix,
}.ScheduleParallel(positions.Length, resolution, dependency);

同样修改一下ScheduleParallel的代码.

1
2
3
4
public delegate JobHandle ScheduleDelegate(
NativeArray<float3x4> positions, NativeArray<float4> noise, //int seed,
Settings settings, SpaceTRS trs, int resolution, JobHandle dependency
);

最后修改下NoiseVisualization类里的代码,用Settings来代替seed.

1
2
3
4
5
6
7
8
9
10
11
[SerializeField]
//int seed;
Settings noiseSettings = Settings.Default;



protected override void UpdateVisualization(NativeArray<float3x4> positions, int resolution, JobHandle handle)
{
noiseJobs[(int)type, dimensions - 1](positions, noise, noiseSettings, domain, resolution, handle).Complete();
noiseBuffer.SetData(noise);
}

Seed nested inside noise settings.


1.2 频率(Frequency)

目前我们仍然只对每个点进行一次噪声采样.图案的缩放是由域缩放控制的.这个缩放也被称为噪声的频率,它体现出噪声变化的快慢.频率或缩放值越高,它的变化越快,因此其特征越小.

我们为方便控制频率,添加一个Settings字段,并把频率限制为int整数,原因稍后会说明.频率必须是正数,并且最小为1,使用属性Min来限制范围,目前默认为4.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

using UnityEngine;

using static Unity.Mathematics.math;

public static partial class Noise
{
[Serializable]
public struct Settings
{
public int seed;

[Min(1)]
public int frequency;

public static Settings Default => new Settings
{
frequency = 4
};
}


}

Seed nested inside noise settings.


Noise.Job中的Execute方法中使用frequencyposition进行缩放.

1
2
3
4
5
6
7
public void Execute(int i)                 
{
float4x3 position = domainTRS.TransformVectors(transpose(positions[i]));
var hash = SmallXXHash4.Seed(settings.seed);
int frequency = settings.frequency;
noise[i] = default(N).GetNoise4(frequency * position, hash);
}

从现在开始,频率和域的缩放都可以影响噪声.频率的缩放是等比的,而域的缩放可以是不等比的,甚至还能是负的.

1.3 音阶(Octaves)

分形噪声由不同频率的多个样本组成.这些被称为Octaves.完美的分形噪声有无限多的Octaves,但我们必一一计算每一个,所以顶多只能支持几个.Octaves数越多,噪声的细节就越多,但生成所需要的时间也更长.所以我们添加一个整数字段来控制Octaves的个数.这里至少应该有一个Octaves,合理的最大值是6,1是个不错的默认值.

1
2
3
4
5
6
7
8
[Range(1, 6)]
public int octaves;

public static Settings Default => new Settings
{
frequency = 4,
octaves = 1
};

Octaves set to 3.


现在修改下Noise.Job.Execute,做一个octaves的循环,每次循环调用GetNoise4,然后将频率翻倍.把所有的采样加在一起,作为最终的噪声值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void Execute(int i)
{
float4x3 position = domainTRS.TransformVectors(transpose(positions[i]));
var hash = SmallXXHash4.Seed(settings.seed);
int frequency = settings.frequency;
float4 sum = 0f;

for (int o = 0; o < settings.octaves; o++)
{
sum += default(N).GetNoise4(frequency * position, hash);
frequency *= 2;
}

noise[i] = sum;
}

这里使用值噪声的截图来演示结果,因为它生成的块状octaves图案比柏林噪声的更容易看清楚.

One, two, and three octaves of 2D value noise.


当我们把相同强度的多个octaves相加后,频率较高的将在计算结果中占主导地位.但是分形噪声的定义是,一个octaves的振幅应该随着其频率的增加而减少.因此,每当我们把频率放大一倍,同时也应该把噪声的振幅减半.

1
2
3
4
5
6
7
8
9
10
int frequency = settings.frequency;
float amplitude = 1f;
float4 sum = 0f;

for (int o = 0; o < settings.octaves; o++)
{
sum += amplitude * default(N).GetNoise4(frequency * position, hash);
frequency *= 2;
amplitude *= 0.5f;
}

此外,多个octaves相加产生的噪声会超过[-1,1].所以我们应该将结果标准化,用octaves和除以振幅之和.

1
2
3
4
5
6
7
8
9
10
11
12
float amplitude = 1f, amplitudeSum = 0f;
float4 sum = 0f;

for (int o = 0; o < settings.octaves; o++)
{
sum += amplitude * default(N).GetNoise4(frequency * position, hash);
amplitudeSum += amplitude;
frequency *= 2;
amplitude *= 0.5f;
}

noise[i] = sum / amplitudeSum;

One, two, and three octaves with decreasing amplitude and normalization.


1.4 给每个音阶分配不同的种子(Unique Seeds Per Octave)

我们目前把不同缩放系数的噪声叠加到了一起.看到了一个奇怪的现象,在域的原点坍缩成了一个奇点.明显感觉到每个缩放等级都有重复的特征,并且收敛在原点处.

Six octaves with the same seed.


我们可以让octaves使用不同的hash值来消除这种视觉假象.继续累加SmallXXHash4accumulator字段就行,这样可以有效地对每个octaves使用连续的种子.

SmallXXHash4增加一个+运算符的静态方法.直接向accumulator累加一个整数,就可以产生不同的hash值了.

1
public static SmallXXHash4 operator + (SmallXXHash4 h, int v) => h.accumulator + (uint)v;

然后在Noise.Job.Executeoctaves循环中把迭代器的值传给GetNoise4,加到hash后面.

1
sum += amplitude * default(N).GetNoise4(frequency * position, hash + o);

Six octaves with different seeds.


1.5 间隙度(Lacunarity)

我们并不总是需要把各个连续的octaves的频率翻倍.频率的缩放比被称为噪声的间隙度.在Settings中添加一个lacunarity的int字段,范围限制在[2,4],默认为2.

1
2
3
4
5
6
7
8
9
[Range(2, 4)]
public int lacunarity;

public static Settings Default => new Settings
{
frequency = 4,
octaves = 1,
lacunarity = 2
};

Lacunarity set to 2.


间隙度(lacunarity)是什么意思?

在当前的项目中,lacunarity是对分形如何填充空间的一种几何描述.lacunarity越高,octaves之间的缝隙越多,或者空白空间越大.它源于拉丁语的lacuna,意思是缝隙(gap)或湖泊(lake).

Noise.Job.Execute中使用lacunarity它来缩放频率,而不总是将频率翻倍.

1
2
3
4
sum += amplitude * default(N).GetNoise4(frequency * position, hash + o);
frequency *= settings.lacunarity;
amplitude *= 0.5f;
amplitudeSum += amplitude;

Lacunarity 2, 3, and 4; frequency 2 with three octaves.


1.6 韧性(Persistence)

就像我们的lacunarity是可以动态配置的,而不是永远都是2一样.octaves之间的振幅也要设计成可配置模式,而不是一直都是0.5.这个因子被称为韧性(Persistence),用来表达幅度在每个octaves间的衰减速度.它是一个float型数据,范围为[0,1].把它加到Settings中然后默认值为0.5.

1
2
3
4
5
6
7
8
9
10
[Range(0f, 1f)]
public float persistence;

public static Settings Default => new Settings
{
frequency = 4,
octaves = 1,
lacunarity = 2,
persistence = 0.5f
};

Persistence set to 0.5.


Noise.Job.Execute中应用persistence的数据,而不是硬编码为0.5.

1
2
frequency *= settings.lacunarity;
amplitude *= settings.persistence;

Persistence 0.25, 0.5, and 0.75; frequency 4 with 3 octaves.


2 扰动(Turbulence)

一个常见的分析噪声变体是把各个音阶的绝对值相加.这会让音阶在通过0点时反弹,形成一个折痕.叠加多个这样音阶的效果被Ken Perlin成为扰动模式,因此这通常被称为柏林噪声的扰动变体.

2.1 Evaluation After Interpolation

为了得到一个音阶的绝对值,需要在插值后对梯度噪声进行进一步计算.目前我们可以把这个操作按梯度算法归纳为一个通用的流程.所以向IGradient接口中添加一个声明EvaluateAfterInterpolation,参数为一个向量化的噪声,在函数中对其进行评估以得到最后的结果.

1
2
3
4
5
6
7
8
9
10
public interface IGradient
{
float4 Evaluate(SmallXXHash4 hash, float4 x);

float4 Evaluate(SmallXXHash4 hash, float4 x, float4 y);

float4 Evaluate(SmallXXHash4 hash, float4 x, float4 y, float4 z);

float4 EvaluateAfterInterpolation(float4 value);
}

常规的值,柏林梯度函数的实现保持不变,不过需要实现这个新的方法,目前简单地返回参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
public struct Value : IGradient
{


public float4 EvaluateAfterInterpolation (float4 value) => value;
}

public struct Perlin : IGradient
{


public float4 EvaluateAfterInterpolation (float4 value) => value;
}

为了用上这个函数,在GetNoise4中,把插值结果变成此函数的参数传入.Lattice1D,Lattice2DLattice3D类中都要改.

1
2
3
4
var g = default(G);
return g.EvaluateAfterInterpolation(lerp(

));

2.2 泛型扰动(Generic Turbulence)

现在我们可以通过复制柏林噪声和值噪声的代码,并修改它们的EvaluateAfterInterpolation函数,以返回绝对值来创建扰动型变体.然而,与其重复写两次这样的代码,不如给Noise.Gradient引入一个通用的泛型结构,包裹一个任意的梯度类型.它将所有的方法调用转发给通用梯度,只是需要使用abs方法将最终的结果变为绝对值.

1
2
3
4
5
6
7
8
9
10
public struct Turbulence<G> : IGradient where G : IGradient
{
public float4 Evaluate (SmallXXHash4 hash, float4 x) => default(G).Evaluate(hash, x);

public float4 Evaluate (SmallXXHash4 hash, float4 x, float4 y) => default(G).Evaluate(hash, x, y);

public float4 Evaluate (SmallXXHash4 hash, float4 x, float4 y, float4 z) => default(G).Evaluate(hash, x, y, z);

public float4 EvaluateAfterInterpolation (float4 value) => abs(default(G).EvaluateAfterInterpolation(value));
}

2.3 Turbulence Perlin and Value Noise

我们现在可以添加扰动型变体到NoiseVisualization类中,指明Turbulence<Perlin>Turbulence<Value>为变体的类型参数.并添加到Job数组中作为选项.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static ScheduleDelegate[,] noiseJobs =
{
{
Job<Lattice1D<Perlin>>.ScheduleParallel,
Job<Lattice2D<Perlin>>.ScheduleParallel,
Job<Lattice3D<Perlin>>.ScheduleParallel
},
{
Job<Lattice1D<Turbulence<Perlin>>>.ScheduleParallel,
Job<Lattice2D<Turbulence<Perlin>>>.ScheduleParallel,
Job<Lattice3D<Turbulence<Perlin>>>.ScheduleParallel
},
{
Job<Lattice1D<Value>>.ScheduleParallel,
Job<Lattice2D<Value>>.ScheduleParallel,
Job<Lattice3D<Value>>.ScheduleParallel
},
{
Job<Lattice1D<Turbulence<Value>>>.ScheduleParallel,
Job<Lattice2D<Turbulence<Value>>>.ScheduleParallel,
Job<Lattice3D<Turbulence<Value>>>.ScheduleParallel
}
};

然后把噪声类型信息也添加到NoiseType枚举信息中.

1
public enum NoiseType { Perlin, PerlinTurbulence, Value, ValueTurbulence }
- -

Regular Perlin and value noise, and their turbulence variants.


因为绝对值永远不会是负的,所以扰动效果总是灰度模式,并且不以零点为中心.正的偏移与负的偏移看起来完全不同.正的有较圆的波峰,较窄的波谷,而负的有尖锐的波峰,较宽的波谷.相比之下,普通噪声的正负偏移起来很相似,只是正负梯度颜色的朝向不同.

- -

Sphere with 0.2 and −0.2 displacement, both regular 3D Perlin and turbulence variant.


3 瓦片噪声(Tiling Noise)

另一个很有用的噪声变体是创建重复的图案.不过不是为了直接填充一个大区域,而是生成一个个小的纹理或网格来无缝铺满一个大的区域.

想要平铺一个图案,相邻的采样区的样子必须是相同的.由于我们使用的是晶格式网格,可以通过重复相同的跨度来做,让这个序列的长度等于噪声的频率,所以,在任何维度上,频率为4的噪声将每四个跨度重复一次.

下一步,频率和间隙度也必须始终是整数,这就是为什么我们把它们变成int类型.

3.1 Frequency at the Lattice Level

为了使实现重复的效果,我们必须将频率传递给函数INoise.GetNoise4.

1
2
3
4
public interface INoise
{
float4 GetNoise4 (float4x3 positions, SmallXXHash4 hash, int frequency);
}

我们现在准备在较低的层面上应用这个频率,在Noise.Job.Evaluate中分别传入未修改的位置和频率参数,而不是用频率缩放位置.

1
sum += amplitude * default(N).GetNoise4(position, hash + o, frequency);

晶格点是由GetLatticeSpan4函数计算得来的,因此向它添加一个频率参数,并在该函数最开始对频率进行缩放.

1
2
3
4
5
6
static LatticeSpan4 GetLatticeSpan4(float4 coordinates, int frequency)
{
coordinates *= frequency;
float4 points = floor(coordinates);

}

最后修改一下所有Lattice类的GetNoise4函数里的频率参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public struct Lattice1D<G> : INoise where G : struct, IGradient
{
public float4 GetNoise4(float4x3 positions, SmallXXHash4 hash, int frequency)
{
LatticeSpan4 x = GetLatticeSpan4(positions.c0, frequency);


}
}

public struct Lattice2D<G> : INoise where G : struct, IGradient
{
public float4 GetNoise4(float4x3 positions, SmallXXHash4 hash, int frequency)
{
LatticeSpan4
x = GetLatticeSpan4(positions.c0, frequency),
z = GetLatticeSpan4(positions.c2, frequency);


}
}

public struct Lattice3D<G> : INoise where G : struct, IGradient
{
public float4 GetNoise4(float4x3 positions, SmallXXHash4 hash, int frequency)
{
LatticeSpan4
x = GetLatticeSpan4(positions.c0, frequency),
y = GetLatticeSpan4(positions.c1, frequency),
z = GetLatticeSpan4(positions.c2, frequency);


}
}

3.2 Lattice接口(Lattice Interface)

为了同时支持普通和瓦片类型,需要在Noise.Lattice类中加入一个新的ILattice接口,里面添加一个GetLatticeSpan4函数声明.LatticeSpan4结构体也需要改成public,因为接口的访问权限是public.

1
2
3
4
5
6
public struct LatticeSpan4 { … }

public interface ILattice
{
LatticeSpan4 GetLatticeSpan4(float4 coordinates, int frequency);
}

现在写一个继承了ILattice接口的LatticNormal类,并实现其中的方法.

1
2
3
4
5
6
7
public struct LatticeNormal : ILattice
{
public LatticeSpan4 GetLatticeSpan4(float4 coordinates, int frequency)
{

}
}

然后修改Lattice这一系列模板类,使用ILattice作为类型参数约束来获得具体的跨度算法.这意味着现在有了两个模板参数.记得泛型类型约束是一个接一个地写的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public struct Lattice1D<L, G> : INoise where L : struct, ILattice where G : struct, IGradient
{
public float4 GetNoise4(float4x3 positions, SmallXXHash4 hash, int frequency)
{
LatticeSpan4 x = default(L).GetLatticeSpan4(positions.c0, frequency);

}
}

public struct Lattice2D<L, G> : INoise where L : struct, ILattice where G : struct, IGradient
{
public float4 GetNoise4(float4x3 positions, SmallXXHash4 hash, int frequency)
{
var l = default(L);
LatticeSpan4
x = l.GetLatticeSpan4(positions.c0, frequency),
z = l.GetLatticeSpan4(positions.c2, frequency);

}
}

public struct Lattice3D<L, G> : INoise where L : struct, ILattice where G : struct, IGradient
{
public float4 GetNoise4(float4x3 positions, SmallXXHash4 hash, int frequency)
{
var l = default(L);
LatticeSpan4
x = l.GetLatticeSpan4(positions.c0, frequency),
y = l.GetLatticeSpan4(positions.c1, frequency),
z = l.GetLatticeSpan4(positions.c2, frequency);

}
}

3.3 瓦片(Tiling)

为了创建一个平铺网格,复制一份LatticeNormal的代码并将其重命名为LatticeTiling,然后修改下它的GetLatticeSpan4函数,让晶格点在频率处重复跨度.可以通过余数或模操作%,取点的余数除以频率来实现,但是必须在基本梯度值之后进行处理.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public struct LatticeTiling : ILattice
{
public LatticeSpan4 GetLatticeSpan4(float4 coordinates, int frequency)
{
coordinates *= frequency;
float4 points = floor(coordinates);
LatticeSpan4 span;
span.p0 = (int4)points;
span.p1 = span.p0 + 1;
span.g0 = coordinates - span.p0;
span.g1 = span.g0 - 1f;

span.p0 %= frequency;
span.p1 %= frequency;

span.t = coordinates - points;
span.t = span.t * span.t * span.t * (span.t * (span.t * 6f - 15f) + 10f);
return span;
}
}

现在可以添加支持所有噪声的平铺版本,只需要在NoiseVisualization的数组中声明就行了.我只展示了柏林噪声的.

1
2
3
4
5
6
7
8
{
Job<Lattice1D<LatticeNormal, Perlin>>.ScheduleParallel,
Job<Lattice1D<LatticeTiling, Perlin>>.ScheduleParallel,
Job<Lattice2D<LatticeNormal, Perlin>>.ScheduleParallel,
Job<Lattice2D<LatticeTiling, Perlin>>.ScheduleParallel,
Job<Lattice3D<LatticeNormal, Perlin>>.ScheduleParallel,
Job<Lattice3D<LatticeTiling, Perlin>>.ScheduleParallel
},

我们将使用一个bool字段来切换标准和平铺功能,而不是将我们的下拉列表的扩大一倍.第二个数组索引等于两倍维度减去1(平铺),或者2(非平铺).

1
2
3
4
5
6
7
8
9
10
11
12
[SerializeField]
bool tiling;



protected override void UpdateVisualization(NativeArray<float3x4> positions, int resolution, JobHandle handle)
{
noiseJobs[(int)type, 2 * dimensions - (tiling ? 1 : 2)](
positions, noise, noiseSettings, domain, resolution, handle
).Complete();
noiseBuffer.SetData(noise);
}

Tiling enabled.


因为单个瓦片会填满整个单位面积,如果我们想要看到平铺效果,必须并增加域的缩放,才能看到非常明显的重复.

One octave frequency 2 tiling 2D Perlin; domain scale 4.


虽然现在有平铺的现象,但这是不对的,因为目前的效果取决于维度的符号.因此,二维噪声显示了四种不同的图案.出现这种情况是因为余数受到了正负号的影响.为了解决这个问题,我们必须修改下LatticeTiling.GetLatticeSpan4函数.

首先检查第一个点,先判断计算后其余数是否为负数,如果是,方向就错了,我们可以通过在该点上加大频率来解决这个问题.

1
2
3
span.p0 %= frequency;
span.p0 = select(span.p0, span.p0 + frequency, span.p0 < 0);
span.p1 %= frequency;

我们还需要修正第二个点,因为它总是比第一个点在正方向上远一个单位,所以可以基于第一个点来计算.

1
2
3
4
5
6
7
//span.p1 = span.p0 + 1;
span.g0 = coordinates - span.p0;
span.g1 = span.g0 - 1f;

span.p0 %= frequency;
span.p0 = select(span.p0, span.p0 + frequency, span.p0 < 0);
span.p1 = (span.p0 + 1) % frequency;

Correct tiling, one and three octaves.


平铺噪声的主要用途是创建无缝连接的纹理或网格.它还可以通过采样多个3D噪声的2D切片来创建循环2D动画.

- -

Four times the same noise sample, domain scale 1.


是否可以只在一部分维度上平铺,而不是所有

可以,通过在每个维度上引入一个单独的通用晶格参数.所以3D噪声就有三种重复的晶格类型.为了使教程的简单,我将其设置为“要么全有,要么全无”.

3.4 向量化的瓦片(Vectorized Tiling)

整数求余是通过整数除法完成的,而整数除法并没有向量化.因此,瓦片噪声的计算需要对点的数据进行重复利用.先计算每个单点的余数,然后再计算一次,这样做效率很低,而且没有必要.

观察一下跨度的第二个点,我们根本不需要计算这个点的余数.可以通过在第一点上加1,然后检查它是否与频率相等来得到同样的结果.如果频率相等,就开始重复图案.因为图案总是从0点重新开始的.

1
2
3
//span.p1 = (span.p0 + 1) % frequency;
span.p1 = span.p0 + 1;
span.p1 = select(span.p1, 0, span.p1 == frequency);

不过第一个点的余数是必须要算的,但是可以通过浮点数除法来实现,浮点数除法是向量化的,只需要延迟一步转换为整数就行.余数的计算方法是:将原浮点坐标,除以频率,接着强转为整数,再与频率相乘,最后用p0减去它.

1
2
3
//span.p0 %= frequency;
span.p0 -= (int4)(points / frequency) * frequency;
span.p0 = select(span.p0, span.p0 + frequency, span.p0 < 0);

原文授权协议


原文项目仓库地址

我写的单CPU端萌新入坑版项目地址

伪随机算法系列-05 Noise Variants(Fractals and Tiling)

https://tzkt623.github.io/2022/01/18/Pseudorandom Noise-05 Noise Variants/

作者

特兹卡特(Tezcat)

发布于

2022-01-18

更新于

2022-01-21

许可协议